JS 垃圾回收和 v8 的垃圾回收机制有什么区别
为什么有垃圾回收
程序运行需要分配内存,V8 也会申请内存,申请的内存又会分为堆内存和栈内存。而内存空间是有限的,内存空间的重复利用就变得非常重要。
垃圾是什么
js 垃圾回收是自动的,非手动释放,也叫自动内存管理。 程序运行结束后,数据不会再使用了,也就是环境中的变量,不再使用(没有被引用的关系),则在在下一波清理过程中被回收
内存分类-栈
- 是什么:栈存放的是基础数据的数值,和引用类型的地址
- 特点:
- 连续有序:栈空间连续,增加删除只需要移动指针,操作速度很快
- 空间有限:栈满了会抛错
- 何时创建销毁:执行函数时创建,函数执行完毕,栈就会销毁
- 应用场景:push pop
内存分类-堆
- 是什么:堆用于存储 js 中的引用类型
- 特点:容量大,不连续,使用灵活存储操作慢。不需要连续空间,或则会申请内容较大
常用的方式
引用计数法 缺点:无法解决循环引用问题
V8 垃圾回收
分为老生代,和新生代 空间换时间
新生代
生命周期短
频繁
互换
- 存放生命周期比较短的对象数据
- GC 频繁
- 分为 from to, 互相交互切换,当一个变量被交换次数达到 5 次以上, 晋升成为老生带,新生代速度快,容量小
晋升
新生代到老生代
新生代的对象转移到老生代称为晋升 经过一次 GC 还存活的对象 限制达到:对象复制到 To-Space 时,To-Space 的空间达到一定的限制
老生代
晋升
资源大
存放生命周期长的数据容量大,速度慢 来源:晋升、资源大(一些资源比较大的,会直接放在老生带中)
老生代回收方式:标记清除+标记整理相结合
标记清除:容易产生碎片,若需要内存空间较多的对象,会造成内存溢出
标记整理:为解决碎片而生,涉及对象移动,故效率不好,一般 10 次标记清除带来 1 次标记整理
增量标记:垃圾回收与应用程序交替使用,可以暂停恢复,可停顿可拆分
内存泄露
不在使用的变量,并没有得到及时回收释放
内存泄露方式
- 定时器未释放
js
const intervalTime = setInterval(function () {
console.log("------");
}, 2000);
- 闭包未销毁
js
// a 没释放
function fn1() {
let a = 2;
function fn2() {
console.log(++a);
}
return fn2;
}
const fn = fn1();
fn();
fn = null; // 等待回收
- 意外的全局变量
js
// 意外全局变量(文件头部增加 'use strict')
function fn1() {
a = 1;
console.log(a);
}
fn1();
- dom 元素删除引用还存在
js
var elements = {
button: document.getElementById("button"),
image: document.getElementById("image"),
text: document.getElementById("text"),
};
function doStuff() {
image.src = "http://some.url/image";
button.click();
console.log(text.innerHTML);
}
function removeButton() {
document.body.removeChild(document.getElementById("button"));
// 但是还在elements对象里保存着#button的引用, DOM元素还在内存里面。
}
// elements.button = null
内存泄露方法排查
performance 中 Heap 对应的部分就可以看到内存在周期性的回落也可以看到垃圾回收的周期,如果垃圾回收之后的最低值(我们称为 min),min 在不断上涨,那么肯定是有较为严重的内存泄漏问题。
内存泄露防止方法
- 定时器销毁
- 全局变量文件头部增加 use strict
- 闭包使用完毕销毁
- dom 元素删除 暂存变量重置未 null
- EventBus 解绑
- ES6 weakMap weakSet
- 添加监听事件 addEventListener 销毁 removeEventListener
内存为什么会影响性能
内存泄漏会造成卡顿,这种卡顿来自于哪里
内存使用不当造成内存泄漏,会导致垃圾回收频繁被处罚,垃圾回收过程中,会阻塞 JS 本身的执行有全停顿,从而造成卡顿
闭包
- 闭包难以察觉到隐形持有关系,更容易忽略,使用不当可能会导致内存泄漏,并不是闭包直接带来的